package panda.natalia.gothread.suspend.instrument;

import jdk.internal.org.objectweb.asm.ClassReader;
import jdk.internal.org.objectweb.asm.ClassVisitor;

import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.NavigableMap;
import java.util.TreeMap;

import static panda.natalia.gothread.suspend.instrument.GoThreadMethodInstrumentor.ASMAPI;

/**
 * a simple framework of java fiber
 *
 * @Author: scpanda2007
 * @Date: 2019/11/28 15:44
 *
 * 通过读取和分析 java class 类信息, 获取一些我们需要的关键信息, 用于帮助判断和进行后续的代码修改等工作
 *  例如类的祖类之类的 一次分析之后可以存下来 复用
 *
 */
public class MethodDatabase {

    private static final int UNKNOWN = 0;
    private static final int JDK = 1;
    private static final int NONSUSPENDABLE = 2;
    private static final int SUSPENDABLE_ABSTRACT = 3;
    private static final int SUSPENDABLE = 4;

    public SuspendableType isMethodSuspendable(String owner, String name, String desc, int opcode) {
        return null;
    }

    public boolean isAllowBlocking() {
        return false;
    }

    public SuspendableClassifier getClassifier() {
        return this.classifier;
    }

    public ClassEntry getOrCreateClassEntry(String className, String superName) {
        ClassEntry classEntry = classes.get(className);
        if(classEntry == null){
            classEntry = new ClassEntry(superName);
            classes.put(className, classEntry);
        }
        return classEntry;
    }

    public boolean isDebug() {
        return false;
    }

    public boolean isAllowMonitors() {
        return false;
    }

    public void recordSuspendableMethods(String className, ClassEntry classEntry) {
        ClassEntry oldEntry;
        synchronized (this) {
            oldEntry = classes.put(className, classEntry);
        }

        if(oldEntry != null && oldEntry != classEntry){
            if(!oldEntry.equals(classEntry)){
                log(LogLevel.WARNING, "Duplicate class entries with different data for class: %s", className);
            }
        }
    }

    public enum SuspendableType {
        NON_SUSPENDABLE, SUSPENDABLE_SUPER, SUSPENDABLE;
    }

    public void log(LogLevel level, String msg, Object ...args){
        System.out.println(String.format(msg, args));
    }

    public void error(String msg, Throwable throwable){
        System.err.println(msg + throwable.getMessage());
    }


    private final WeakReference<ClassLoader> clRef;
    private final SuspendableClassifier classifier;
    private final GoThreadMethodInstrumentor instrumentor;
    private final NavigableMap<String, ClassEntry> classes;
    private final HashMap<String, String> superClasses;

    public MethodDatabase(GoThreadMethodInstrumentor instrumentor, ClassLoader classLoader, SuspendableClassifier classifier){
        this.instrumentor = instrumentor;
        this.clRef = classLoader != null ? new WeakReference<ClassLoader>(classLoader) : null;
        this.classifier = classifier;

        this.classes = new TreeMap<String, ClassEntry>();
        this.superClasses = new HashMap<String, String>();
    }

    private static final ClassEntry CLASS_NOT_FOUND = new ClassEntry("<class not found>");

    public static final class ClassEntry {

        private final HashMap<String, SuspendableType> methods;

        private String sourceName;

        private String sourceDebugInfo;
        private final String superName;
        private boolean instrumented;
        private boolean requiresInstrumentation;

        public String[] getInterfaces() {
            return interfaces;
        }

        private String[] interfaces;

        public ClassEntry(String superName){
            this.superName = superName;
            this.methods = new HashMap<>();
        }

        public String getSourceName() {
            return sourceName;
        }

        public String getSourceDebugInfo() {
            return sourceDebugInfo;
        }

        public boolean isInstrumented() {
            return instrumented;
        }

        public void setInstrumented(boolean instrumented) {
            this.instrumented = instrumented;
        }

        public String getSuperName() {
            return this.superName;
        }

        public void setInterfaces(String[] interfaces) {
            this.interfaces = interfaces;
        }

        public void setRequiresInstrumentation(boolean requiresInstrumentation) {
            this.requiresInstrumentation = requiresInstrumentation;
        }

        public boolean requiresInstrumentation() {
            return this.requiresInstrumentation;
        }

        @Override
        public int hashCode() { return superName.hashCode() * 67 + methods.hashCode();}

        @Override
        public boolean equals(Object object){
            if(!(object instanceof ClassEntry)){
                return false;
            }
            final ClassEntry other = (ClassEntry) object;
            return superName.equals(other.superName) && methods.equals(other.methods);
        }

        public void setSourceName(String source) {
            this.sourceName = source;
        }

        public void setSourceDebugInfo(String debug) {
            this.sourceDebugInfo = debug;
        }

        public SuspendableType check(String name, String desc){
            return methods.get(key(name, desc));
        }

        private static String key(String methodName, String methodDesc){
            return methodName.concat(methodDesc);
        }

        public void set(String name, String desc, SuspendableType suspendableType) {
            String nameAndDesc = key(name, desc);
            methods.put(nameAndDesc, suspendableType);
        }
    }

    /**
     * 从 Object 向下逐一匹配，直到最后一个相同的类
     * @param classA
     * @param classB
     * @return
     */
    public String getCommonSuperClass(String classA, String classB){
        ArrayList<String> listA = getSuperClasses(classA);
        ArrayList<String> listB = getSuperClasses(classB);
        if(listA == null || listB == null)
            return null;

        int idx = 0;
        int num = Math.min(listA.size(), listB.size());
        for(; idx < num; idx++ ){
            String superClassA = listA.get(idx);
            String superClassB = listB.get(idx);
            if(!superClassA.equals(superClassB)) break;
        }

        if(idx > 0) return listA.get(idx - 1);

        return null;
    }


    public static boolean isProblematicClass(String className) {
        return className.startsWith("org/gradle")
                || className.startsWith("javax/jms/")
                || className.startsWith("ch/qos/logback/")
                || className.startsWith("org/apache/logging/log4j/")
                || className.startsWith("org/apache/log4j/");
    }

    public boolean isException(String className) {
        while (true) {
            if("java/lang/Throwable".equals(className)) return true;
            if("java/lang/Object".equals(className)) return false;
            String superClass = getDirectSuperClass(className);
            if(superClass == null){
                log(isProblematicClass(className) ? LogLevel.INFO : LogLevel.WARNING, "Can't determine super class of %s (this is usually related to classloading)", className);
                return false;
            }
            className = superClass;
        }
    }

    private ArrayList<String> getSuperClasses(String className) {
        ArrayList<String> result = new ArrayList<String>();
        while (true){
            result.add(0, className);
            if("java/lang/Object".equals(className)){
                return result;
            }

            String superClass = getDirectSuperClass(className);
            if(superClass == null){
                log(isProblematicClass(className) ? LogLevel.INFO : LogLevel.WARNING, "Can't determine super class of %s", className);
                return null;
            }
        }
    }

    protected String getDirectSuperClass(String className) {
        ClassEntry entry = getClassEntry(className);
        if(entry != null && entry != CLASS_NOT_FOUND)
            return entry.getSuperName();

        String superClass;
        synchronized (this){
            superClass = superClasses.get(className);
        }

        if(superClass == null){
            superClass = extractSuperClass(className);
            if(superClass != null){
                String oldSuperClass;
                synchronized (this){
                    oldSuperClass = superClasses.put(className, superClass);
                }
                if(oldSuperClass!=null){
                    if(!oldSuperClass.equals(superClass)){
                        log(LogLevel.WARNING, "Duplicate super class entry with different value: %s vs %s", oldSuperClass, superClass);
                    }
                }
            }
        }

        return superClass;
    }

    private String extractSuperClass(String className) {

        ClassLoader cl = null;
        if(clRef != null){
            cl = clRef.get();
            if(cl == null) return null;
        }

        try (final InputStream is = ClassLoaderHelper.getResourceAsStream(cl, className + ".class")){
            ClassReader r = new ClassReader(is);
            ExtractSuperClass esc = new ExtractSuperClass();
            r.accept(esc, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
            return esc.superClass;
        }catch (IOException ioe) {
            error(className, ioe);
        }

        return null;
    }

    public static class ExtractSuperClass extends ClassVisitor {
        String superClass;
        public ExtractSuperClass() { super(ASMAPI); }

        @Override
        public void visit(int version, int access, String name, String signature, String superClass, String[] interfaces) {
            this.superClass = superClass;
        }
    }

    ClassEntry getClassEntry(String className) {
        return classes.get(className);
    }

    public static boolean isInvocationHandlerInvocation(String className, String methodName){
        return className.equals("java/lang/reflect/InvocationHandle") && methodName.startsWith("invoke");
    }

    public static boolean isMethodHandleInvocation(String className, String methodName){
        return className.equals("java/lang/invoke/MethodHandle") && methodName.startsWith("invoke");
    }

    public static boolean isReflectInvocation(String className, String methodName){
        return "java/lang/reflect/Method".equals(className) && "invoke".equals(methodName);
    }

    /**
     * Synthetic 为合成的意思, 这个应该是指的 生成的闭包内部函数 访问外部实例变量 而生成的 access 函数
     *
     * {@link panda.natalia.gothread.suspend.instrument.codesample.TestSyntheticAccess}
     *
     * @param className
     * @param methodName
     * @return
     */
    public static boolean isSyntheticAccess(String className, String methodName){
        return methodName.startsWith("access$");
    }
}
